using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.Security.Principal;
using System.ServiceModel;
using System.ServiceModel.Configuration;
using System.Text;
using PSE.Hosting.ServiceHoster.Configuration;
using System.IO;
using System.ServiceProcess;
using PSE.Hosting.ServiceHoster.Logging;
using PSE.Hosting.ServiceHoster.Business;
using System.Threading;

namespace PSE.Hosting.ServiceHoster.Process
{
    /// <summary>
    /// This class implements a Singleton pattern to manage the services.
    /// </summary>
    public sealed class Services
    {
        #region Fields

        private const string NoServiceAssemblyMessage = "<This assembly doesn't have any service running.>";
        private static volatile Services _instance;
        private static object _syncRoot = new Object();
        private Dictionary<string, AssemblyState> _assemblies = new Dictionary<string, AssemblyState>();

        #endregion

        #region Public Members

        public static Services Instance
        {
            get
            {
                if (_instance == null)
                {
                    lock (_syncRoot)
                    {
                        if (_instance == null)
                        {
                            _instance = new Services();
                        }
                    }
                }
                return _instance;
            }
        }

        /// <summary>
        /// Start all services a specified package.
        /// </summary>
        /// <param name="packageName">The package name to start the services.</param>
        /// <param name="initialize">Initialize the assenblies.</param>
        public List<MessageData> StartAll(string packageName, bool initialize)
        {
            List<MessageData> messagesList = new List<MessageData>();

            lock (_assemblies)
            {
                try
                {
                    packageName = packageName.ToUpper();

                    List<AssemblyElement> assemblies = ConfigurationFile.Instance.GetAssemblies(packageName);

                    // Start the assebblies of this package.
                    foreach (AssemblyElement element in assemblies)
                    {
                        List<MessageData> messages = InternalStartAssembly(packageName, element.CodeBase, initialize);
                        messagesList.AddRange(messages);
                    }
                }
                catch (Exception ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    throw;
                }
            }

            return messagesList;
        }

        /// <summary>
        /// Stop all the services from an assembly.
        /// </summary>
        /// <param name="packageName">The name of the package containing the assembly to be stopped.</param>
        /// <param name="codeBase">The name of the assembly to be stopped.</param>
        public void StopAssembly(string packageName, string codeBase)
        {
            lock (_assemblies)
            {
                try
                {
                    // If the assembly is not started, it does not need to be stopped.
                    if (!_assemblies.ContainsKey(Path.GetFileName(codeBase)))
                    {
                        return;
                    }

                    // Stops all the services from this assembly
                    AssemblyState state = _assemblies[Path.GetFileName(codeBase)];
                    state.StopAll();
                }
                catch (Exception ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    throw;
                }
            }
        }

        /// <summary>
        /// Start all WCF services from a specific assembly.
        /// </summary>
        /// <param name="packageName">The name of the package of the assembly to start.</param>
        /// <param name="fileName">The name of the assembly file to start. This is the filename only, without the path.</param>
        /// 
        public List<MessageData> StartAssembly(string packageName, string fileName)
        {
            try
            {
                AssemblyElement element = ConfigurationFile.Instance.GetAssembly(packageName, fileName);
                return this.InternalStartAssembly(packageName, element.CodeBase, true);
            }
            catch (PSE.Hosting.ServiceHoster.Configuration.InvalidAssemblyException ex)
            {
                throw new PSE.Hosting.ServiceHoster.Business.InvalidAssemblyException(ex.Message, ex);
            }
            catch (Exception ex)
            {
                Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                throw;
            }
        }

        /// <summary>
        /// Starts a Service configured within the Hoster app.config.
        /// </summary>
        /// <param name="assembly">The assembly name.</param>
        /// <param name="serviceName">The service name to start.</param>
        public void StartService(string packageName, string assembly, string serviceName)
        {
            lock (_assemblies)
            {
                if (!_assemblies.ContainsKey(assembly))
                    throw new ArgumentOutOfRangeException("assembly", string.Format("Assembly is not loaded {0}.", assembly));

                try
                {
                    AssemblyState state = _assemblies[assembly];
                    state.Start(serviceName);
                }
                catch (Exception ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    throw;
                }
            }
        }

        /// <summary>
        /// Stops the specified service.
        /// </summary>
        /// <param name="packageName">The name of the package containing the service.</param>
        /// <param name="assembly">The assembly name.</param>
        /// <param name="serviceName">The service name to stop.</param>
        public void StopService(string packageName, string assembly, string serviceName)
        {
            lock (_assemblies)
            {
                if (!_assemblies.ContainsKey(assembly))
                    throw new ArgumentOutOfRangeException("assembly", string.Format("Assembly is not loaded {0}.", assembly));

                try
                {
                    AssemblyState state = _assemblies[assembly];
                    state.Stop(serviceName);
                }
                catch (Exception ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    throw;
                }
            }
        }

        /// <summary>
        /// Stops all the services already started from a specific package.
        /// </summary>
        public void StopAll(string packageName)
        {
            lock (_assemblies)
            {
                try
                {
                    foreach (AssemblyState assembly in _assemblies.Values)
                    {
                        assembly.StopAll();
                    }
                }
                catch (Exception ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    throw;
                }
            }
        }

        /// <summary>
        /// Gets a list of all the services configured within the Service Hoster app.config.
        /// </summary>
        /// <returns></returns>
        public List<Entities.Service> GetServiceList(string packageName, string assembly)
        {
            lock (_assemblies)
            {
                if (!_assemblies.ContainsKey(assembly))
                    throw new ArgumentOutOfRangeException("assembly", string.Format("Assembly is not loaded {0}.", assembly));

                try
                {
                    AssemblyState state = _assemblies[assembly];

                    List<Entities.Service> list = new List<Entities.Service>();

                    Dictionary<string, ServiceData> services = state.GetServices();

                    if (services == null || services.Count == 0)
                    {
                        list.Add(new Entities.Service(NoServiceAssemblyMessage, false, state.AssemblyFullName, state.CodeBase, String.Empty, false));
                    }
                    else
                    {
                        foreach (KeyValuePair<string, ServiceData> service in services)
                        {
                            list.Add(new Entities.Service(service.Key, service.Value.IsAlive, state.AssemblyFullName, state.CodeBase, service.Value.MexAddress, true));
                        }
                    }

                    return list;
                }
                catch (Exception ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    throw;
                }
            }
        }

        /// <summary>
        /// Returns a list of all the assemblies added in a package.
        /// </summary>
        /// <param name="packageName">The name of the package to get the list of assenblies.</param>
        /// <returns>List of added assemblies.</returns>
        public List<Entities.Assembly> GetAssemblyList(string packageName)
        {
            lock (_assemblies)
            {
                try
                {
                    List<Entities.Assembly> response = new List<Entities.Assembly>();

                    foreach (AssemblyState assembly in _assemblies.Values)
                    {
                        Entities.Assembly entity = new Entities.Assembly();
                        entity.FileName = assembly.FileName;
                        entity.Name = assembly.AssemblyFullName;
                        entity.Path = assembly.CodeBase;
                        entity.IsValid = assembly.IsValid;
                        response.Add(entity);
                    }

                    return response;
                }
                catch (Exception ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    throw;
                }
            }
        }

        /// <summary>
        /// Adds a Service to the Service Hoster config file.
        /// </summary>
        /// <param name="packageName">The name of the package.</param>
        /// <param name="codeBase">The assembly code base.</param>
        public void AddAssembly(string packageName, string codeBase)
        {
            lock (_assemblies)
            {
                try
                {
                    ConfigurationFile.Instance.AddAssembly(packageName, codeBase);

                    AssemblyElement assembly = ConfigurationFile.Instance.GetAssembly(packageName, Path.GetFileName(codeBase));

                    _assemblies.Add(Path.GetFileName(assembly.CodeBase), new AssemblyState(packageName, assembly.CodeBase));
                }
                catch (PSE.Hosting.ServiceHoster.Configuration.InvalidAssemblyException ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    throw;
                }
                catch (Exception ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    throw;
                }
            }
        }

        /// <summary>
        /// Removes a Service from the Service Hoster config file.
        /// </summary>
        /// <param name="packageName">The name of the package.</param>
        /// <param name="assemblyFile">The name of the assembly file (without path).</param>
        public void RemoveAssembly(string packageName, string assemblyFile)
        {
            lock (_assemblies)
            {
                if (!_assemblies.ContainsKey(assemblyFile))
                    throw new ArgumentOutOfRangeException("assembly", string.Format("Assembly is not loaded {0}.", assemblyFile));

                try
                {
                    string fullPath = String.Empty;

                    AssemblyState state = _assemblies[assemblyFile];
                    fullPath = state.CodeBase;
                    state.StopAll();
                    _assemblies.Remove(assemblyFile);

                    // Removes the assembly from the configuration file.
                    ConfigurationFile.Instance.RemoveAssembly(packageName, fullPath);

                    ReloadConfigurationFile();

                    Logger.Log(string.Format("Assembly {0} was removed.", assemblyFile), Logger.EntryType.Information, Logger.Source.ServiceHoster);
                }
                catch (Exception ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    throw;
                }
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Initializes a service state.
        /// </summary>
        /// <param name="packageName">The package name.</param>
        /// <param name="assembly">The service assembly name.</param>
        /// <param name="codeBase">The assembly code base.</param>
        private List<MessageData> InternalStartAssembly(string packageName, string codeBase, bool initialize)
        {
            List<MessageData> messages = new List<MessageData>();

            AssemblyState state = null;

            try
            {
                lock (_assemblies)
                {
                    if (_assemblies.ContainsKey(Path.GetFileName(codeBase)))
                    {
                        state = _assemblies[Path.GetFileName(codeBase)];
                    }
                    else
                    {
                        state = new AssemblyState(packageName, codeBase);
                        _assemblies.Add(Path.GetFileName(codeBase), state);
                    }
                }

                if (initialize)
                {
                    state.IsValid = true;

                    state.AssemblyChanged += new AssemblyEventHandler(AssemblyState_AssemblyChanged);
                    state.AssemblyDeleted += new AssemblyEventHandler(AssemblyState_AssemblyDeleted);

                    Dictionary<string, Exception> exceptions = state.StartAll();

                    foreach (KeyValuePair<string, Exception> exception in exceptions)
                    {
                        StringBuilder error = new StringBuilder();
                        error.Append(DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"));
                        error.AppendLine(" - ERROR STARTING SERVICE ->" + codeBase);
                        error.AppendLine("======================================================================================");
                        error.AppendLine("Service: " + exception.Key);
                        error.AppendLine("Message: " + exception.Value.Message);

                        if (exceptions[exception.Key].InnerException != null)
                            error.AppendLine("Inner Exception: " + exception.Value.InnerException.Message);

                        error.AppendLine();

                        messages.Add(new MessageData(MessageData.MessageTypes.Error, error.ToString()));

                        Logger.Log(exception.Value, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                    }
                }

                Dictionary<string, ServiceData> services = state.GetServices();

                if (services != null && services.Count > 0)
                {
                    StringBuilder message = new StringBuilder();
                    message.Append(DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"));
                    message.AppendLine(" - ASSEMBLY STARTED ->" + codeBase);
                    message.AppendLine();

                    messages.Add(new MessageData(MessageData.MessageTypes.Success, message.ToString()));
                }
            }
            catch (NoServiceInAssemblyException)
            {
                if (state != null)
                    state.IsValid = false;
            }
            catch (Exception exception)
            {
                StringBuilder error = new StringBuilder();
                error.Append(DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss"));
                error.AppendLine(" - ERROR LOADING ASSEMBLY ->" + codeBase);
                error.AppendLine("======================================================================================");
                error.AppendLine("Message: " + exception.Message);
                if (exception.InnerException != null)
                    error.AppendLine("Inner Exception: " + exception.InnerException.Message);
                error.AppendLine();

                if (state != null)
                    state.IsValid = false;

                messages.Add(new MessageData(MessageData.MessageTypes.Error, error.ToString()));

                Logger.Log(exception, Logger.EntryType.Error, Logger.Source.ServiceHoster);
            }

            return messages;
        }

        private void AssemblyState_AssemblyDeleted(object source, AssemblyEventArgs eventArgs)
        {
            using (Mutex mutex = new Mutex(false, Constants.MutexId))
            {
                try
                {
                    mutex.WaitOne();

                    RemoveAssembly(eventArgs.PackageName, Path.GetFileName(eventArgs.CodeBase));
                }
                catch (Exception ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                }
                finally
                {
                    mutex.ReleaseMutex();
                }
            }
        }

        private void AssemblyState_AssemblyChanged(object source, AssemblyEventArgs eventArgs)
        {
            using (Mutex mutex = new Mutex(false, Constants.MutexId))
            {
                try
                {
                    mutex.WaitOne();

                    AssemblyState state = (AssemblyState)source;

                    state.StopAll();

                    ReloadConfigurationFile();

                    if (Path.GetExtension(eventArgs.CodeBase) == ".dll")
                    {
                        state.InitializeServiceInstance();
                    }

                    state.RefreshSections();

                    state.StartAll();
                }
                catch (Exception ex)
                {
                    Logger.Log(ex, Logger.EntryType.Error, Logger.Source.ServiceHoster);
                }
                finally
                {
                    mutex.ReleaseMutex();
                }
            }
        }

        private void ReloadConfigurationFile()
        {
            Program.DeleteConfigurationFile();

            foreach (AssemblyState assembly in _assemblies.Values)
            {
                Program.MergeConfigurationFile(assembly.CodeBase + ".config");
            }
        }

        #endregion
    }
}
